package com.zub.config;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.management.*;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The god object that is the core of Configer.  This singleton does most of the heavy lifting
 *
 * @author miken
 * @version 1.0 Jul 16, 2009
 */
public class Configer {
    private final static Log log = LogFactory.getLog(ConfigerAdapterMBean.class);
    private static final String ENV = "env";
    private static final String PRODUCTION = "production";

    private static Configer self = new Configer();

    private Environment currentEnvironment;
    private Map<String, Environment> environments;

    private Configer() {
        //an anonymous subclass of HashMap which only exists to provide a nice toString() method for help text
        environments = new HashMap<String, Environment>() {
            public String toString() {
                //its OK here not to use a stringbuffer since we won't ever have more than a few environments
                String stringForm = new String();
                for(String environment : this.keySet()) {
                    stringForm += environment + ", ";
                }
                stringForm.substring(0, stringForm.length()-2);
                return stringForm;
            }
        };
    }

    public void addEnvironment(Environment environment) {
        if (environments.containsKey(environment.getName())) {
            throw new InvalidEnvironmentConfigurationException(environment.getName() + " is already defined.  Names must be " +
                    "unique  Known environments are: " + environments.keySet());
        }
        environments.put(environment.getName(), environment);
    }

    public void bootstrap() {
        //verify that we have a production configuration
        if (!environments.containsKey(PRODUCTION)) {
            throw new InvalidEnvironmentConfigurationException(PRODUCTION + " configuration must be defined.  Known " +
                    "environments are: " + environments.keySet());
        }

        final String envProperty = System.getProperty(ENV);
        //if we're null, then we're in production
        if (envProperty == null) {
            currentEnvironment = environments.get(PRODUCTION);
        } else {
            //if its not null, try to find the requested environment in the map
            if (environments.containsKey(envProperty)) {
                currentEnvironment = environments.get(envProperty);
            } else {
                throw new InvalidEnvironmentConfigurationException("Environment " + envProperty + " was not found.  " +
                        "Known environments are: " + environments.toString());
            }
        }

        //print help if desired
        //display help
        if (System.getProperty("help") != null) {
            System.out.println("Current environment is " + currentEnvironment.getName());
            System.out.println("Supported environments are: " + environments.toString());
            System.out.println("");
            System.out.println("production environment is used by default.  To select an environment specify the 'env' " +
                    "JVM parameter.  Examples: -Denv=dev or -Denv=stage");
            System.out.println("");
            System.out.println("Use -D JVM parameters to override any of these default values for this environment.");
            for (String key : currentEnvironment.keySet()) {
                System.out.println("\t" + key + " = " + currentEnvironment.get(key));
            }
            System.out.println("");
            System.exit(0);
        }

        //register ourself with JMX
        MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        ObjectName name = null;
        try {
            name = new ObjectName("com.zub.config:type=ConfigerAdapterMBean");
        } catch (MalformedObjectNameException e) {
            throw new RuntimeException("Hard coded object name caused MalformedObjectNameException.  " +
                    "There is probably an error in this code.", e);
        }

        ConfigerAdapterMBean configerAdapterMBean = new ConfigerAdapter();
        try {
            mBeanServer.registerMBean(configerAdapterMBean, name);
        } catch (InstanceAlreadyExistsException e) {
            log.debug("It looks like the Configer MBean is already registered.  This is probably not an issue", e);
        } catch (MBeanRegistrationException e) {
            log.warn("Could not register Configer MBean.  You will not be able to manage Configer over JMX.", e);
        } catch (NotCompliantMBeanException e) {
            log.warn("Could not create Configer MBean.  You will not be able to manage Configer over JMX.", e);
        }

    }

    /**
     * Apply the same configuration to all environments
     */
    @Deprecated
    public static Configer bootstrap(Environment all) {
        return Configer.bootstrap(all, all, all, all);
    }

    /**
     * The correct way to bootstrap Configer for an environment dependent component.
     */
    @Deprecated
    public static Configer bootstrap(Environment dev, Environment stage, Environment demo, Environment production) {
        return Configer.bootstrap(dev, stage, demo, production, false);
    }

    /**
     * This should only be used inside a unit test!
     */
    @Deprecated
    public static Configer bootstrapForTest(Environment all) {
        return Configer.bootstrap(all, all, all, all, true);
    }

    /**
     * This should only be used inside a unit test to clear the Configer state!
     */
    @Deprecated
    public static void destroyForTest() {
        self = null;
    }

    /**
     * This should only be used inside a unit test to clear the Configer state!
     */
    public static void clearForTest() {
        self = new Configer();
    }

    @Deprecated
    private static Configer bootstrap(Environment dev, Environment stage, Environment demo, Environment production, boolean testMode) {
        if (self == null) {
            self = new Configer();
        } else if(testMode == false) {
            assert false : "Attempted to bootstrap again using deprecated methods.  Bootstrap should only be run once";
        }
        //fix all of the passed in Environments by setting the correct name
        if (dev != null) {
            dev.setName("dev");
            self.addEnvironment(dev);
        }
        if (stage != null) {
            stage.setName("stage");
            self.addEnvironment(stage);
        }
        if (demo != null) {
            demo.setName("demo");
            self.addEnvironment(demo);
        }
        if (production != null) {
            //we used to support two names for production so clone it to support both
            Environment productionClone = new Environment();
            productionClone.setName("prod");
            //TODO: move clone into Environemnt class
            for(String key : production.keySet()) {
                productionClone.set(key, production.get(key));
            }
            self.addEnvironment(productionClone);

            production.setName("production");
            self.addEnvironment(production);            
        }

        self.bootstrap();
        return self;
    }

    /**
     * get an instance of Configer
     *
     * @return The singleton Configer instance
     */
    public static Configer getInstance() {
        return self;
    }

    /**
     * Get the value for a configuration property.  Property values can be set or overridden from the command line.  If
     * no default value is found, fall back to the default values specified in the map hard coded into this class.
     *
     * @param propertyName
     * @return The String value for the property or null if the property is not set.
     */
    public String getConfig(String propertyName) {
        assert currentEnvironment != null : "Configer is not set up.  Run Configer.bootstrap()!";

        if (System.getProperty(propertyName) != null) {
            return System.getProperty(propertyName);
        } else {
            return currentEnvironment.get(propertyName);
        }
    }

    /**
     * Get the value for a required configuration property.  This works just like getConfig, but if the property is not
     * set anywhere, an exception will be thrown.
     *
     * @param propertyName
     * @return String value of the requested property
     * @throws RequiredConfigNotFoundException
     *          Thrown when property is not found
     */
    public String getRequiredConfig(String propertyName) throws RequiredConfigNotFoundException {
        assert currentEnvironment != null : "Configer is not set up.  Run Configer.bootstrap()!";

        if (System.getProperty(propertyName) != null) {
            return System.getProperty(propertyName);
        } else if (currentEnvironment.containsKey(propertyName)) {
            return currentEnvironment.get(propertyName);
        } else {
            throw new RequiredConfigNotFoundException("Required config [" + propertyName + "] was not found.");
        }
    }

    /**
     * Get the int value for a required configuration property.  This works just like getConfig, but if the property is
     * not set anywhere, an exception will be thrown.
     *
     * @param propertyName
     * @return int value of the requested property
     * @throws RequiredConfigNotFoundException
     *                               Thrown when property is not found
     * @throws NumberFormatException Thrown when a proeprty that is not a long is requested in such format
     */
    public int getRequiredConfigInt(String propertyName) throws RequiredConfigNotFoundException {
        assert currentEnvironment != null : "Configer is not set up.  Run Configer.bootstrap()!";

        if (System.getProperty(propertyName) != null) {
            try {
                return Integer.parseInt(System.getProperty(propertyName));
            } catch (NumberFormatException e) {
                throw new NumberFormatException(String.format("Requested command line config [%s] with value [%s] could " +
                        "not be returned as a long", propertyName, System.getProperty(propertyName)));
            }
        } else if (currentEnvironment.containsKey(propertyName)) {
            try {
                return Integer.parseInt(currentEnvironment.get(propertyName));
            } catch (NumberFormatException e) {
                throw new NumberFormatException(String.format("Requested default config [%s] with value [%s] could not " +
                        "be returned as a long", propertyName, System.getProperty(propertyName)));
            }
        } else {
            throw new RequiredConfigNotFoundException("Required config [" + propertyName + "] was not found.");
        }
    }

    public String getCurrentEnvironment() {
        assert currentEnvironment != null : "Configer is not set up.  Run Configer.bootstrap()!";

        return currentEnvironment.getName();
    }

    public List list() {
        assert currentEnvironment != null : "Configer is not set up.  Run Configer.bootstrap()!";

        return new ArrayList(currentEnvironment.keySet());
    }

    public Map getAll() {
        assert currentEnvironment != null : "Configer is not set up.  Run Configer.bootstrap()!";

        return currentEnvironment.toMap();
    }

    public void setConfig(String property, String newValue) throws RequiredConfigNotFoundException {
        assert currentEnvironment != null : "Configer is not set up.  Run Configer.bootstrap()!";

        if (!currentEnvironment.containsKey(property)) {
            throw new RequiredConfigNotFoundException("Specified property " + property + " can not be set because it did not exist at startup.");
        }
        currentEnvironment.set(property, newValue);
    }
}